import { isEqual, capitalize } from '@src/utils/util';
import { isDef } from '@src/utils/shared';

let uid = 0;

export default class Node {
    constructor(data, config, parentNode) {
        this.data = data;
        this.config = config;
        this.parent = parentNode || null;
        this.level = !this.parent ? 1 : this.parent.level + 1;
        this.uid = uid++;

        this.initState();
        this.initChildren();
    }

    initState() {
        const { value: valueKey, label: labelKey } = this.config;

        this.value = this.data[valueKey];
        this.label = this.data[labelKey];
        this.pathNodes = this.calculatePathNodes();
        this.path = this.pathNodes.map(node => node.value);
        this.pathLabels = this.pathNodes.map(node => node.label);

        // lazy load
        this.loading = false;
        this.loaded = false;
    }

    initChildren() {
        const { config } = this;
        const childrenKey = config.children;
        const childrenData = this.data[childrenKey];
        this.hasChildren = Array.isArray(childrenData);
        this.children = (childrenData || []).map(child => new Node(child, config, this));
    }

    get isDisabled() {
        const { data, parent, config } = this;
        const disabledKey = config.disabled;
        const { checkStrictly } = config;
        return data[disabledKey] ||
      !checkStrictly && parent && parent.isDisabled;
    }

    get isLeaf() {
        const { data, loaded, hasChildren, children } = this;
        const { lazy, leaf: leafKey } = this.config;
        if (lazy) {
            const isLeaf = isDef(data[leafKey])
                ? data[leafKey]
                : (loaded ? !children.length : false);
            this.hasChildren = !isLeaf;
            return isLeaf;
        }
        return !hasChildren;
    }

    calculatePathNodes() {
        const nodes = [this];
        let parent = this.parent;

        while (parent) {
            nodes.unshift(parent);
            parent = parent.parent;
        }

        return nodes;
    }

    getPath() {
        return this.path;
    }

    getValue() {
        return this.value;
    }

    getValueByOption() {
        return this.config.emitPath
            ? this.getPath()
            : this.getValue();
    }

    getText(allLevels, separator) {
        return allLevels ? this.pathLabels.join(separator) : this.label;
    }

    isSameNode(checkedValue) {
        const value = this.getValueByOption();
        return this.config.multiple && Array.isArray(checkedValue)
            ? checkedValue.some(val => isEqual(val, value))
            : isEqual(checkedValue, value);
    }

    broadcast(event, ...args) {
        const handlerName = `onParent${capitalize(event)}`;

        this.children.forEach(child => {
            if (child) {
                // bottom up
                child.broadcast(event, ...args);
                child[handlerName] && child[handlerName](...args);
            }
        });
    }

    emit(event, ...args) {
        const { parent } = this;
        const handlerName = `onChild${capitalize(event)}`;
        if (parent) {
            parent[handlerName] && parent[handlerName](...args);
            parent.emit(event, ...args);
        }
    }

    onParentCheck(checked) {
        if (!this.isDisabled) {
            this.setCheckState(checked);
        }
    }

    onChildCheck() {
        const { children } = this;
        const validChildren = children.filter(child => !child.isDisabled);
        const checked = validChildren.length
            ? validChildren.every(child => child.checked)
            : false;

        this.setCheckState(checked);
    }

    setCheckState(checked) {
        const totalNum = this.children.length;
        const checkedNum = this.children.reduce((c, p) => {
            const num = p.checked ? 1 : (p.indeterminate ? 0.5 : 0);
            return c + num;
        }, 0);

        this.checked = checked;
        this.indeterminate = checkedNum !== totalNum && checkedNum > 0;
    }

    syncCheckState(checkedValue) {
        const value = this.getValueByOption();
        const checked = this.isSameNode(checkedValue, value);

        this.doCheck(checked);
    }

    doCheck(checked) {
        if (this.checked !== checked) {
            if (this.config.checkStrictly) {
                this.checked = checked;
            } else {
                // bottom up to unify the calculation of the indeterminate state
                this.broadcast('check', checked);
                this.setCheckState(checked);
                this.emit('check');
            }
        }
    }
}
